feat: smart PCH rebuild, #include/import completion, rapid-edit robustness - #394
Conversation
When the user is mid-edit in the preamble region (e.g. typing an #include path that isn't closed yet), skip the PCH rebuild and reuse the existing PCH. This avoids wasteful builds with incomplete code. - Add is_preamble_complete() that checks all #include/#import directives have properly closed "" or <> delimiters - In ensure_pch, when preamble hash changed but content is incomplete, defer rebuild and keep using the old PCH Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds preamble-completeness detection, master-level include/import completion, and PCM-build triggering from scanned in-memory buffers; updates master server logic to prefer cached PCH when preamble is incomplete and to handle include/import completions locally. Changes
Sequence Diagram(s)mermaid mermaid Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/syntax/scan.cpp (1)
483-491: CRLF line endings may cause false positives.The
split('\n')approach doesn't strip\rfrom Windows-style line endings. If a user types#include\r\n(incomplete directive with CRLF),after_keywordwould be"\r"instead of empty, causing the check at line 518 to incorrectly treat it as complete (macro case).Consider trimming the line on both sides or explicitly handling
\r:🔧 Suggested fix
while(!preamble.empty()) { auto [line, rest] = preamble.split('\n'); preamble = rest; - auto trimmed = line.ltrim(); + auto trimmed = line.ltrim().rtrim("\r");
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: c451e53e-bd0b-403a-b59f-39a68c35ac6c
📒 Files selected for processing (3)
src/server/master_server.cppsrc/syntax/scan.cppsrc/syntax/scan.h
Also defer PCH/PCM rebuild when the user is typing incomplete module statements like `import std` (missing `;`) or `export module ` (incomplete module name). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Include/import completion: - Intercept completion requests in master when cursor is on #include or import line, handle directly without forwarding to stateless worker - #include completion: enumerate headers from SearchConfig + DirListingCache, support subdirectory navigation (e.g. "sys/ty" prefix) - import completion: filter path_to_module by prefix, insert with ";" Buffer-aware module dependencies: - In ensure_deps, scan buffer text for import statements to discover module dependencies not yet known to compile_graph (user added import without saving) - Build needed PCMs on-the-fly before compilation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- detect_completion_context: return early after # branch so #import (Objective-C) doesn't fall through to C++20 import detection - is_preamble_complete: add word boundary check so "important" etc. don't match "import"/"export" keywords Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/server/master_server.cpp`:
- Around line 683-709: The current use of scan(text) is ineffective because
scan() doesn't populate scan_result.modules (it only extracts includes/module
declarations), so the loop over scan_result.modules is a no-op; fix by either
replacing the call to scan(text) with scan_precise(text, args) (using the same
compiler arguments used elsewhere and wiring through the required preprocess
step so PreciseScanPPCallbacks::moduleImport() fills modules) or by extending
scan() to recognize and record import directives into its result; update the
code around scan_result.modules, keep the existing behavior of attempting to
build missing PCMs (pcm_paths, compile_graph->compile_deps(pid)), and ensure the
new call provides/accepts the necessary compiler arguments used by scan_precise
so imported modules are discovered at runtime.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: da4f9542-e95a-4816-91a1-b1ec82f7aaef
📒 Files selected for processing (2)
src/server/master_server.cppsrc/server/master_server.h
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
src/server/master_server.cpp (1)
683-709:⚠️ Potential issue | 🟠 Major
scan(text)does not populatemodules— this code path is ineffective.As noted in a previous review, the lexer-based
scan()function only extracts#includedirectives and module declarations (module_name,is_interface_unit). It does not handleimportstatements — those are only captured byscan_precise()through itsPreciseScanPPCallbacks::moduleImport()callback.The
ScanResult::modulesvector (seesrc/syntax/scan.h:50) is only populated byscan_precise(), not byscan(). Therefore,scan_result.moduleswill always be empty here, making the entire iteration block unreachable dead code.To discover
importstatements from the in-memory buffer, either:
- Use
scan_precise()(requires compilation arguments, more expensive)- Extend
scan()to extractcxx_import_decl/cxx_export_import_decldirectives,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/server/master_server.cpp` around lines 683 - 709, The current buffer-scan uses scan(text) but ScanResult::modules is only filled by scan_precise(), so the loop is dead; fix by either (A) switching this path to call scan_precise(...) with the appropriate compilation arguments and then use the returned ScanResult.modules (leveraging PreciseScanPPCallbacks::moduleImport), or (B) extend the lightweight scan(...) implementation to also recognize C++20 import/export import directives (cxx_import_decl / cxx_export_import_decl) and populate ScanResult::modules so the existing logic (checking path_to_module, pcm_paths, compile_graph->compile_deps(pid)) works; update references in this block to use the chosen function (scan_precise or the enhanced scan) and ensure ScanResult::modules is actually filled before iterating.
🧹 Nitpick comments (1)
src/syntax/scan.cpp (1)
479-491: Missing word-boundary check for#include/#importdirective keywords.The function checks
directive.starts_with("include")ordirective.starts_with("import")without verifying a word boundary, meaning a hypothetical directive like#includefoowould be incorrectly treated as an include directive. While this is an unlikely edge case in practice, it's inconsistent with the word-boundary check applied for C++20import/exportkeywords inis_preamble_complete(lines 521-524).Also, the keyword length calculation at line 482/489 assumes "import" (6 chars) or "include" (7 chars), but
#importin Objective-C uses 6 chars. If a#include_nextdirective were passed here, the offset would be wrong.🔧 Suggested improvement for robustness
static bool is_include_directive_complete(llvm::StringRef directive) { + // Determine keyword and its length + llvm::StringRef keyword; + if(directive.starts_with("include_next") && + (directive.size() == 12 || !llvm::isAlnum(directive[12]))) { + keyword = "include_next"; + } else if(directive.starts_with("include") && + (directive.size() == 7 || !llvm::isAlnum(directive[7]))) { + keyword = "include"; + } else if(directive.starts_with("import") && + (directive.size() == 6 || !llvm::isAlnum(directive[6]))) { + keyword = "import"; + } else { + return true; // Not a recognized include/import directive + } + if(directive.contains('"')) { - auto after_keyword = directive.drop_front(directive.starts_with("import") ? 6 : 7); + auto after_keyword = directive.drop_front(keyword.size()); return after_keyword.count('"') >= 2; } if(directive.contains('<')) { return directive.contains('>'); } // No " or < — might be a macro (`#include` FOO) or just incomplete (`#include` ). - auto after_keyword = directive.drop_front(directive.starts_with("import") ? 6 : 7).ltrim(); + auto after_keyword = directive.drop_front(keyword.size()).ltrim(); return !after_keyword.empty(); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/syntax/scan.cpp` around lines 479 - 491, is_include_directive_complete currently uses directive.starts_with("import")/("include") without ensuring a word boundary and then hardcodes lengths 6/7; update is_include_directive_complete to first detect which keyword actually matches ("import" vs "include") and verify a word boundary by checking the character after the keyword is end-of-string, whitespace, '"' or '<' (so "#include_next" won't be treated as "include"); compute the keyword length from that matched keyword and use it when computing after_keyword (instead of always using 6/7) so Objective-C "#import" and other edge cases are handled correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/server/master_server.cpp`:
- Around line 1152-1200: detect_completion_context incorrectly treats
identifiers like "important" as the import keyword because it uses
import_check.consume_front("import") without a word-boundary check; update the
logic in detect_completion_context so you only accept "import" when the next
character (if any) is not an alphanumeric (use llvm::isAlnum or equivalent)
before consuming/returning and keep returning {CompletionContext::Import,
import_check.str()} when matched; additionally, handle Objective‑C "#import" in
the preprocessor branch (the block that currently checks
directive.consume_front("include")) so that if directive.consume_front("import")
succeeds you treat it like an include directive (return the appropriate
IncludeQuoted or IncludeAngled context based on the following quote/angle)
instead of falling through.
---
Duplicate comments:
In `@src/server/master_server.cpp`:
- Around line 683-709: The current buffer-scan uses scan(text) but
ScanResult::modules is only filled by scan_precise(), so the loop is dead; fix
by either (A) switching this path to call scan_precise(...) with the appropriate
compilation arguments and then use the returned ScanResult.modules (leveraging
PreciseScanPPCallbacks::moduleImport), or (B) extend the lightweight scan(...)
implementation to also recognize C++20 import/export import directives
(cxx_import_decl / cxx_export_import_decl) and populate ScanResult::modules so
the existing logic (checking path_to_module, pcm_paths,
compile_graph->compile_deps(pid)) works; update references in this block to use
the chosen function (scan_precise or the enhanced scan) and ensure
ScanResult::modules is actually filled before iterating.
---
Nitpick comments:
In `@src/syntax/scan.cpp`:
- Around line 479-491: is_include_directive_complete currently uses
directive.starts_with("import")/("include") without ensuring a word boundary and
then hardcodes lengths 6/7; update is_include_directive_complete to first detect
which keyword actually matches ("import" vs "include") and verify a word
boundary by checking the character after the keyword is end-of-string,
whitespace, '"' or '<' (so "#include_next" won't be treated as "include");
compute the keyword length from that matched keyword and use it when computing
after_keyword (instead of always using 6/7) so Objective-C "#import" and other
edge cases are handled correctly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 8eb8bca2-f6f6-4d74-93a5-79190525bc8b
📒 Files selected for processing (2)
src/server/master_server.cppsrc/syntax/scan.cpp
…egration tests Unit tests (19 cases): - #include completeness: quoted, angled, macro, empty, multiple - C++20 module: import, export module, export import with/without ; - Edge cases: empty preamble, word boundary (important != import), mixed Integration tests (5 cases): - Quoted include completion with prefix filtering - Subdirectory include completion (subdir/nested.h) - Angled include completion for system headers - No include completion on regular code - Empty prefix lists all local headers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@tests/integration/test_include_completion.py`:
- Around line 130-135: The test currently skips validation when result is None;
add an explicit assertion that result is not None before extracting items/labels
so the negative-path test fails if completion is completely broken.
Specifically, in tests/integration/test_include_completion.py around the block
using the result variable, insert an assert result is not None (or use assert
result) prior to computing items = result.items if hasattr(result, "items") else
result and the subsequent labels checks to ensure the test always validates the
expected non-include completions.
- Around line 161-164: The test's assertion list doesn't match the comment: when
prefix is empty the completion should include both "myheader.h" and the
directory entry "subdir/"; update the assertions in
tests/integration/test_include_completion.py to also assert that "subdir/" is
present in the labels variable (in addition to the existing assert "myheader.h"
in labels) so the test verifies both file and directory entries are returned.
In `@tests/unit/syntax/scan_tests.cpp`:
- Around line 414-418: The test MixedIncludeAndImportAllComplete currently uses
compute_preamble_bound(content) which returns a bound that stops before the
"import std;" line so the import isn't being validated; change the test to set
the bound to cover the import (for example use size_t bound = content.size() or
otherwise compute a bound that includes the "import std;" token) before calling
is_preamble_complete(content, bound) so both the `#include` and import are
checked.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 1e7c080b-b7c2-4bc7-8541-de111be66ef1
📒 Files selected for processing (6)
tests/conftest.pytests/data/include_completion/main.cpptests/data/include_completion/myheader.htests/data/include_completion/subdir/nested.htests/integration/test_include_completion.pytests/unit/syntax/scan_tests.cpp
✅ Files skipped from review due to trivial changes (3)
- tests/data/include_completion/subdir/nested.h
- tests/data/include_completion/myheader.h
- tests/conftest.py
…tests 4 integration test cases: - Import completion basic: type "import " → lists known module "A" - Import completion with prefix: "import A" → filters to module A - Import completion dotted names: "import my." → shows my.app, my.io - Buffer-aware module deps: add import in buffer without saving, verify PCM is built and compilation succeeds Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
getFileEntryRefForID can return an invalid entry for certain FileIDs (e.g. built-in buffers, remapped files). The old code had an assert that was optimized out in RelWithDebInfo, leading to a null deref in FileEntryRef::getName(). Return empty string for invalid entries and skip them in deps collection. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Default worker counts: 3 stateless, 2 stateful (was cpu/4 each) - drain_stderr demoted to LOG_DEBUG — workers have their own log files, master.log no longer contains duplicated worker output. drain_stderr still captures crash/assertion output at debug level. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- test_preamble_edit_then_hover: edit preamble (add #include), verify AST still works after PCH rebuild - test_preamble_edit_multiple_times: 3 consecutive preamble edits, verify no errors accumulate Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add DEBUG logging to forward_stateful, forward_stateless, and ensure_compiled with path, version, generation, ast_dirty state - Add didChange debug log with version and generation - Log early-exit reasons (ensure_compiled failed, worker error, etc.) - Add test_preamble_edit_then_hover and test_preamble_edit_multiple_times Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Ignore SIGPIPE at startup so writing to closed pipes returns EPIPE instead of killing the process (macOS CI crash) - Change test_preamble_edit_then_hover to add a comment instead of #include <cstdio> — avoids slow system header PCH build in CI Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove std::signal(SIGPIPE, SIG_IGN) from main — not appropriate - Restore #include <cstdio> in test_preamble_edit_then_hover - Fix clang-format on logging arguments in master_server.cpp Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: when multiple ensure_compiled() coroutines waited on doc.compiling->wait() and the in-flight compile finished with a generation mismatch, ALL waiters woke up simultaneously. Each one saw ast_dirty=true and fell through to start its OWN compile request, flooding the stateful worker and causing IPC deadlock. Fix: change the compiling wait from `if` to `while` loop — after waking, re-check doc.compiling before starting a new compile. Only the first waiter starts a compile; the rest loop back and wait on the new completion event. Also: - Add BuildPCH diagnostic error messages to worker logs - Update hello_world/main.cpp to include <iostream> (realistic test) - Fix hardcoded line numbers in test_server.py and test_file_operation.py - Add test_rapid_edits_with_hover: 50 rapid edits + hover each time - Move publish_diagnostics after finish_compile to unblock waiters faster Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Launch compile as a detached task (loop.schedule) so LSP $/cancelRequest cannot kill in-flight compilations and leave doc.compiling stuck forever - Add RAII CompileGuard to ensure doc.compiling is always cleaned up - Drop stale feature requests when ast_dirty after ensure_compiled - Add is_preamble_complete() to defer PCH rebuilds during incomplete edits - Add #include and import completion intercepted at master level - Log BuildPCH diagnostic errors for debuggability Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Rename edit.jsonl → rapid_edit.jsonl and add to repo - Remove unused kWorkerRequestTimeout constant - Document why timeout is disabled (eventide spurious cancellation bug) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace #include <cstdio> with #include "common.h" in test_preamble_edit_then_hover to avoid slow PCH rebuilds on macOS CI that cause SIGPIPE timeouts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… test - Add word boundary check in detect_completion_context so identifiers like "important" are not mistaken for "import" keyword - Make negative-path include completion test assert non-null result Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
LSP clients may close the pipe at any time (editor exit, test teardown). Without this, writing to the closed pipe kills the server with signal 13 instead of returning EPIPE. Guarded with #ifndef _WIN32 for portability. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Preamble completeness check
is_preamble_complete()inscan.cpp: checks whether#include/import/export moduledirectives in the preamble region are syntactically complete (have closing>/"/;)ensure_pchdefers PCH rebuild when preamble is incomplete (user still typing), reuses old PCH instead of failing#include / import completion
#include "..."/#include <...>/import ...contexts before forwarding to workercomplete_include(): searches include paths (from compile args viaSearchConfig) usingDirListingCache, supports quoted/angled/multi-level pathscomplete_import(): filterspath_to_modulemap by prefiximportantnot treated asimport)Detached compile task (rapid-edit fix)
ensure_deps+send_stateful+publish_diagnostics) run as detached tasks vialoop.schedule(), independent of the LSP request coroutine chain$/cancelRequestcan no longer kill in-flight compilations — previously, cancellation would destroy theensure_compiledcoroutine frame, leavingdoc.compilingpermanently set and hanging all subsequent requestsCompileGuardRAII ensuresdoc.compilingis always cleaned up even if the detached task failsast_dirtybecame true after compile finished) are dropped before forwarding to workerOther fixes
signal(SIGPIPE, SIG_IGN)on POSIX: prevents server crash when LSP client disconnects mid-writeCompilationUnitRef::file_path()/deps(): null-checkFileEntryRefto prevent segfault on invalid FileIDstateless_worker.cpp: log BuildPCH diagnostic errors for debuggabilitylogging_dirdefault changed to.clice/logsin configTests
is_preamble_complete(incomplete#include,import,export module, mixed cases)test_include_completion.py(5 tests),test_import_completion.py(4 tests),test_rapid_edit.py(2 tests),test_pch.py(4 new tests)rapid_edit.jsonl— recorded VSCode session with 40 rapid edits + 61 cancel requestsTest plan
#include <iostream>project🤖 Generated with Claude Code